Retropikzel's blog - 2025-11-29 - foreign-c status report, started R6RS support

Version 0.12.0 of (foreign c) is out, so I thought it would be good time to write a bit about what is going on.

Table of contents:

Migration to Codeberg

R7RS-large work happens in Codeberg, and Guix is there too. So I guess I just want to hang out with the cool kids. :)

I have moved the code from Sourcehut to Codeberg under foreign-c organization.

R6RS Support

Did you know that Akku has library called akku-r7rs? Like it says on the page it is a R7RS standard library R6RS schemes. And it’s a fantastic idea.

Akku uses it when mirroring packages from Snow Fort. And as you can see for example on (retropikzel system) in Akku repositories, it is added to them as dependency automatically. The library in snow-fort only depends on (foreign c).

Now youre propably wondering why I’m telling you this, or if you read the title maybe not. Using Akku I have started adding R6RS support to (foreign c), it’s still in it’s infancy but thanks to Akku it’s very feasible.

Bad news first so at the time of writing on the R6RS side Racket, Mosh and Ypsilon wont even start to run tests for now. I’m working on it you all Racket, Mosh and Ypsilon enjoyes, dont worry. They all run the R7RS versions so it’s jut a matter of bending things to fit. :)

But R6RS Schemes that do start to run tests (not passing all) are:

Good library to test is (retropikzel system). It is the C system function wrapped. Given main.sps:

(import (rnrs)
        (retropikzel system))

(system "ls")

Run:

akku install "(retropikzel system)"
.akku/env
$SCHEME main.sps

It should list the files in current directory and show the magic of portable foreign function interface (or erorr messages :D). Also make sure that the version of (foreign c) installed is 0.12.0 or above. If not then you can download the version 0.12.0. Copy the “foreign” directory to same directory as the main.sps and run akku install without any arguments.

Other things that might be interesting to test are (srfi 170) and (retropikzel requests). But at this point I can not make guaranteens that they work. However there is not anything fundamental standing in the way fo them working, just bugs.

Working on dynamic C FFI for Gauche

I already had Gauche support made earlier, but installing it required user to download the library and run make to compile and install it. Compared to all other implementations this is just not good enough. As a lazy person myself, trust me, it’s not. :) Any other implementation does not require extra work.

Gauche has “static”, and very good, C FFI, which I’m using to make the dynamic C FFI.

The related issue is here Dynamic C Foreign Function Interface #1168. The code is already in quite good shape, but I have to add tests and do cleanup before I dare to submit a pull request.

Code restructuring

I have also resctrucuted the code and put the implementation specific primitives into their own libaries and named them $SCHEME-primitives.sld. Usually code restructuring would propably not be something to write about, but in this case I thought I’d share what each primitives file exports:

(export size-of-type
        align-of-type
        shared-object-load
        define-c-procedure
        c-bytevector?
        c-bytevector-u8-ref
        c-bytevector-u8-set!
        c-bytevector-pointer-ref
        c-bytevector-pointer-set!)

The reason I’m showing you this is that from those 9 exports of primitives we can make these (non final, WIP ones removed) exports of the whole library:

(export
    ;; Primitives
    c-type-size
    c-type-align
    define-c-library
    define-c-procedure

    make-c-bytevector

    ;; Primitives
    c-bytevector?
    c-bytevector-u8-set!
    c-bytevector-u8-ref
    c-bytevector-pointer-set!
    c-bytevector-pointer-ref

    make-c-null
    c-null?
    c-free
    call-with-address-of

    bytevector->c-bytevector
    c-bytevector->bytevector

    string->c-utf8
    c-utf8->string

    libc-name

    native-endianness
    c-bytevector-s8-set!
    c-bytevector-s8-ref

    c-bytevector-char-set!
    c-bytevector-char-ref
    c-bytevector-uchar-set!
    c-bytevector-uchar-ref

    c-bytevector-sint-set!
    c-bytevector-sint-ref
    c-bytevector-uint-set!
    c-bytevector-uint-ref

    c-bytevector-s16-set!
    c-bytevector-s16-ref
    c-bytevector-u16-set!
    c-bytevector-u16-ref
    c-bytevector-s16-native-set!
    c-bytevector-s16-native-ref
    c-bytevector-u16-native-set!
    c-bytevector-u16-native-ref

    c-bytevector-s32-set!
    c-bytevector-s32-ref
    c-bytevector-u32-set!
    c-bytevector-u32-ref
    c-bytevector-s32-native-set!
    c-bytevector-s32-native-ref
    c-bytevector-u32-native-set!
    c-bytevector-u32-native-ref

    c-bytevector-s64-set!
    c-bytevector-s64-ref
    c-bytevector-u64-set!
    c-bytevector-u64-ref
    c-bytevector-s64-native-set!
    c-bytevector-s64-native-ref
    c-bytevector-u64-native-set!
    c-bytevector-u64-native-ref

    c-bytevector-ieee-single-native-set!
    c-bytevector-ieee-single-native-ref

    c-bytevector-ieee-single-set!
    c-bytevector-ieee-single-ref

    c-bytevector-ieee-double-set!
    c-bytevector-ieee-double-ref

    c-bytevector-ieee-double-native-set!
    c-bytevector-ieee-double-native-ref)

If the c-bytevector-* procedures look familiar it is because it entirely based on the (r6rs bytevectors) from Snow Fort. Interface design is hard to get right and the (r6rs bytevectors) already uses one u8 primitive. On R7RS it is bytevector-u8-ref, on (foreign c) it is c-bytevector-u8-ref. So the library was a good fit to allow familiar and extensive interface to C pointers. It also removed some bugs some implementations had with some numerical access to pointers by bypassing the Scheme implementation on any other than u8 access to pointer.

Making scope more reasonable for 1.0.0, misc versioning thoughts

For the primitives of calling functions and accessing pointer, the scheme implementations usually offer enough to hoist (foreign c) on top. But when it comes to C callbacks only some implementations support them. And since supporting different things on different implementations is not portable. I have decided that callback support should propably be moved into 1.X.0.

I think that having a common way to do C FFI. That is stable, and boring in a good way. Is more important than having every functionality right now. Implementing callback support is not impossible, it will just take a long time. I need to go implementation by implementation and add it to many of them. I’m also looking forward to it, I’m sure I will learn a lot. But I also recognize that it will take a while. And during that time I would like to have, to myself and others, a C FFI library on top of which libraries can be built on knowing the interface will not change under them.

If it happens that I have made a huge mistake and the (foreign c) interface needs to be changed in non backwards compatible way (after 1.0.0) then I have been thinking that it would propably be best to add (foreign c 2) or (foreign c2), however stupid it might look like. And leave the (foreign c) as version 1. Which can then be used and all old libraries would continue to work.

About structs and arrays

Speaking of 1.0.0 you might look at the long export list above and notice the lack of any struct or array procedures. The reason to that is that they can be built on top of these exported procedures. I will make something for structs and arrays, I need them for myself too. But the changes I will get those right enough to justify adding them to the “core” (foreign c) is low. The changes that I will get those right enough so that people also like them? Even lower. :D So I think it’s best I leave them as optional, propably even as separate libraries that can then reach their own 1.0.0 after I have actually had the change to use them, “perfect” the interface design and work out the quirks.

This also of course means that you can build your own struct and array libraries. Your needs and skills vary from mine so I think thats a good thing.

Misc and happy hacking

Some notes that might be of interest.

Passing structs by value is also unsolved thing. Which will need to happen in 1.X.0 later.

I was asked if I’m considering making (foreign c) an SRFI, and I’m not sure yet.

Thats it for now, happy hacking! :)